import com.sun.media.ui.MessageBox;
import com.sun.media.ui.MessageBox;
import java.util.Vector;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.swing.JOptionPane;

/**
 * Class that handles the encryption and decryption of data
 */
public class Encrypter {
    // The encryption cypher
    private Cipher encryption_cypher;
    // The decryption cypher
    private Cipher decryption_cypher;
    // The size of a chunk of raw unencrypted data
    private int MAX_CHUNK = 100;
    
    /**
     * The class constructor
     */
    public Encrypter() {
        try {
            // Gets an instance of the chosen encryption cypher
            encryption_cypher = Cipher.getInstance(Key.typeEncryption2String(Settings.key.type_encryption));
            // Gets an instance of the chosen decryption cypher
            decryption_cypher = Cipher.getInstance(Key.typeEncryption2String(Settings.key.type_encryption));
            
            // If the key is for a symmetric cypher
            if(Settings.key.type_key == Key.TypeOfKey.Symmetric) {
                encryption_cypher.init(Cipher.ENCRYPT_MODE, Settings.key.symmetric_key);
                decryption_cypher.init(Cipher.DECRYPT_MODE, Settings.key.symmetric_key);
            } else { // If the key is for an assymetric cypher
                // If the Public key was chosen for cryptography operations
                if(Settings.public_key_selected) {
                    encryption_cypher.init(Cipher.ENCRYPT_MODE, Settings.key.public_key);
                    decryption_cypher.init(Cipher.DECRYPT_MODE, Settings.key.public_key);
                } else { // If the Private key was chosen for cryptography operations
                    encryption_cypher.init(Cipher.ENCRYPT_MODE, Settings.key.private_key);
                    decryption_cypher.init(Cipher.DECRYPT_MODE, Settings.key.private_key);
                }
            }
        } catch (javax.crypto.NoSuchPaddingException e) {
            e.printStackTrace();
        } catch (java.security.NoSuchAlgorithmException e) {
            e.printStackTrace();
        } catch (java.security.InvalidKeyException e) {
            e.printStackTrace();
        }
    }
    
    /**
     * Encrypts or decrypts an array of bytes and adds an MD5 hash at the start of the file
     * @return Array of bytes with the processed byte stream (encrypted or decrypted)
     * @param is_decypher Indicates if the stream should be encrypted or decrypted
     * @param original The array of bytes to transform
     */
    public byte [] crypt(byte [] original, boolean is_decypher) {
        try {
            // Choose appropriate provider
            Cipher cipher = is_decypher ? decryption_cypher : encryption_cypher;
            
            // If it's a cyphering attempt, then get the original hash
            if(!is_decypher)
                original = Utilities.insertChecksum(original);
            
            // Decide what's the size of each manageable chunk
            int chunk_size = (is_decypher) ? Settings.key.getEncryptedBlockSize() : MAX_CHUNK;
            
            // Partition data into chunks and encrypt/decypt them
            Vector <byte []> original_chunks = Utilities.partitionData(original,chunk_size);
            Vector <byte []> processed_chunks = new Vector <byte []>();
            for(int x = 0; x < original_chunks.size(); x++)
                processed_chunks.add(cipher.doFinal(original_chunks.get(x)));
            
            byte [] processed = Utilities.mergeData(processed_chunks);
            
            // If it's in decipher mode, then check if the file is corrupted, by comparing the MD5 hashes
            if(is_decypher) {
                byte [] data =  Utilities.extractData(processed);
                byte [] md5 = Utilities.extractMD5(processed);
                byte [] file_md5 = Utilities.md5(data);
                for(int x = 0; x < md5.length; x++)
                    if(md5[x] != file_md5[x])
                        return null;
                
                return data;
            }
            
            return processed;
        } catch (BadPaddingException ex) {
            ex.printStackTrace();
        } catch (IllegalBlockSizeException ex) {
            ex.printStackTrace();
        }
        
        return null;
    }
    
    /**
     * Encrypts an array of bytes
     * @param dec Array of bytes to encrypt
     * @return Returns the array of bytes in an encrypted format
     */
    public byte [] encrypt(byte [] dec) {
        return crypt(dec,false);
    }
    
    /**
     * Decrypts an array of bytes
     * @param dec Array of bytes to decrypt
     * @return Returns the array of bytes in a decrypted format
     */
    public byte [] decrypt(byte [] dec) {
        return crypt(dec,true);
    }
    
    /**
     * Encrypts a file and stores it
     * @param input_file File to encrypt
     * @param output_file Destination for the encrypted file
     */
    public void encryptFile(String input_file, String output_file) {
        Storage.saveFile(output_file,encrypt(Storage.readFile(input_file)));
    }
    
    /**
     * Decrypts a file and stores it
     * @param input_file File to decrypt
     * @param output_file Destination for the decrypted file
     */
    public void decryptFile(String input_file, String output_file){
        byte [] dec = decrypt(Storage.readFile(input_file));
        if(dec == null)
            JOptionPane.showMessageDialog(GUI.frame,"The file is not genuine or is corrupted, and therefore can't be decrypted.","File corrupted",JOptionPane.WARNING_MESSAGE);
        else
            Storage.saveFile(output_file,dec);
    }
}
